﻿using GodSharp.Communications.Abstractions;
using GodSharp.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;

namespace GodSharp.Communications.Udp
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="destination"></param>
    /// <param name="buffer"></param>
    /// <param name="sender"></param>
    internal delegate void UdpCommunicationDataEventHandler(string destination, byte[] buffer, UdpSender sender);
    
    /// <summary>
    /// 
    /// </summary>
    /// <param name="sender"></param>
    public delegate void UdpCommunicationClientConnectedEventHandler(UdpSender sender);

    /// <summary>
    /// 
    /// </summary>
    /// <param name="sender"></param>
    public delegate void UdpCommunicationClientDisconnectedEventHandler(UdpSender sender);

    /// <summary>
    /// 
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="exception"></param>
    public delegate void UdpCommunicationExceptionEventHandler(UdpSender sender,Exception exception);

#pragma warning disable CS1584 // XML comment has syntactically incorrect cref attribute
#pragma warning disable CS1658 // Warning is overriding an error
    /// <summary>
    /// UdpCommunicationProvider
    /// </summary>
    /// <seealso cref="GodSharp.Communications.Abstractions.CommunicationProviderBase{GodSharp.Communications.Udp.UdpCommunicationProviderOptions}" />
    /// <seealso cref="GodSharp.Communications.Abstractions.ICommunicationProvider" />
    [NameDescription("UdpCommunicationProvider", "Udp protocol for GodSharp.Communications")]
#pragma warning restore CS1658 // Warning is overriding an error
#pragma warning restore CS1584 // XML comment has syntactically incorrect cref attribute
    public sealed class UdpCommunicationProvider : CommunicationProviderBase<UdpCommunicationProviderOptions, IUdpCommunicationHandler>, IUdpCommunicationProvider
    {
        private Dictionary<string, UdpCommunicationDataEventHandler> OnData { get; set; }

        UdpCommunicationClientConnectedEventHandler OnServerClientConnected;
        UdpCommunicationClientDisconnectedEventHandler OnServerClientDisconnected;
        UdpCommunicationClientConnectedEventHandler OnClientConnected;
        UdpCommunicationClientDisconnectedEventHandler OnClientDisconnected;
        UdpCommunicationExceptionEventHandler OnException;

        Dictionary<int, UdpClient> clients = null;
        UdpConfig[] cConfigs = null;

        Dictionary<int, UdpSender> senders = null;

        bool initialized = false;
        bool started = false;

        /// <summary>
        /// Initializes a new instance of the <see cref="UdpCommunicationProvider"/> class.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        public UdpCommunicationProvider(UdpCommunicationProviderOptions parameter)
        {
            Initialize(parameter);
        }

        /// <summary>
        /// Initializes the specified parameter.
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">
        /// parameter
        /// or
        /// ServerClientConfigs
        /// </exception>
        /// <exception cref="InvalidOperationException">The provider is initialized</exception>
        /// <exception cref="ArgumentException">
        /// Configs
        /// or
        /// Configs
        /// </exception>
        private bool Initialize(UdpCommunicationProviderOptions parameter)
        {
            if (parameter == null) throw new ArgumentNullException(nameof(parameter));

            OnServerClientConnected = parameter.ServerClientConnectedHandler;
            OnServerClientDisconnected = parameter.ServerClientDisconnectedHandler;
            OnClientConnected = parameter.ClientConnectedHandler;
            OnClientDisconnected = parameter.ClientDisconnectedHandler;
            OnException = parameter.ExceptionHandler;

            if (parameter == null || parameter.Configs.Count < 1) throw new ArgumentNullException(nameof(parameter));

            if (initialized) throw new InvalidOperationException("The provider is initialized");

            if (parameter.Configs.GroupBy(x => x.Id).Any(x => x.ToArray().Length > 1)) throw new ArgumentException($"Duplicate 'Id' at {nameof(parameter.Configs)}");

            UdpConfig[] configs = parameter.Configs.Where(x => x.Available).ToArray();

            cConfigs = configs.Where(x => x.ConfigType == UdpConfigFlags.Client).ToArray();

            if (cConfigs?.Length > 0)
            {
                clients = new Dictionary<int, UdpClient>();

                foreach (var item in cConfigs)
                {
                    if (clients.ContainsKey(item.Id)) continue;

                    clients.Add(item.Id, new UdpClient(item.Host, item.Port) { OnData = SocketClientDataHandler, OnException = SocketExceptionHandler });
                }
            }

            initialized = true;

            return initialized;
        }

        /// <summary>
        /// Starts the specified objs.
        /// </summary>
        /// <param name="ids">The id array.</param>
        /// <returns></returns>
        public bool Start(params int[] ids)
        {
            CheckingState(true, false, true);
                        
            List<int> cKeys = new List<int>();

            if (ids?.Length > 0)
            {
                if (clients?.Count > 0) cKeys = clients.Keys.Where(x => ids.Contains(x)).ToList();
            }
            else
            {
                cKeys = clients?.Keys.ToList();
            }
            
            if (cKeys?.Count > 0)
            {
                foreach (var key in cKeys)
                {
                    try
                    {                        
                        clients[key].Start();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }
            }

            senders = new Dictionary<int, UdpSender>();

            return started;
        }

        /// <summary>
        /// Stops the specified objs.
        /// </summary>
        /// <param name="ids">The id array.</param>
        /// <returns></returns>
        public bool Stop(params int[] ids)
        {
            CheckingState(true, true);

            if (clients?.Count > 0)
            {
                List<string> keys = new List<string>();

                foreach (var item in clients)
                {
                    try
                    {
                        item.Value.Stop();
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Res the start.
        /// </summary>
        /// <param name="ids">The id array.</param>
        /// <returns></returns>
        public bool Restart(params int[] ids)
        {
            CheckingState(true, true);

            Stop();

            Start();

            return true;
        }

        /// <summary>
        /// Healthes this instance.
        /// </summary>
        /// <returns></returns>
        public CommunicationModuleStateFlags Health()
        {
            CheckingState(true, true);

            return CommunicationModuleStateFlags.Starting;
        }

        /// <summary>
        /// Healthes the specified objs.
        /// </summary>
        /// <param name="ids">The id array.</param>
        /// <returns></returns>
        public CommunicationModuleStateFlags[] Health(params int[] ids)
        {
            CheckingState(true, true);

            return null;
        }

        /// <summary>
        /// Sockets the data handler.
        /// </summary>
        /// <param name="client">The client.</param>
        /// <param name="buffer">The buffer.</param>
        private void SocketClientDataHandler(UdpSender client, byte[] buffer)
        {
            try
            {
                Console.WriteLine($"Tcp:{client.Guid} Received from server {client.RemoteEndPoint}");

                IPEndPoint point = client.RemoteEndPoint as IPEndPoint;

                UdpConfig config = cConfigs.FirstOrDefault(x => x.Host == point.Address.ToString() && x.Port == point.Port);

                SocketDataHandler(config, client, buffer);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        /// <summary>
        /// Sockets the data handler.
        /// </summary>
        /// <param name="config">The configuration.</param>
        /// <param name="client">The client.</param>
        /// <param name="buffer">The buffer.</param>
        private void SocketDataHandler(UdpConfig config, UdpSender client, byte[] buffer)
        {
            if (config == null)
            {
                return;
            }

            OnData[config.EntryPoint]?.Invoke(config.EntryPoint, buffer, client);
        }

        /// <summary>
        /// Sockets the server client connected handler.
        /// </summary>
        /// <param name="client">The client.</param>
        private void SocketServerClientConnectedHandler(UdpSender client)
        {
            Console.WriteLine($"Tcp:{client.Guid} client->server[{client.RemoteEndPoint}] connected");

            UdpConfig config = GetSocketConfig(client, UdpTypeFlags.Server);

            if (config != null)
            {
                RemoveSender(config.Id);

                senders.Add(config.Id, client);
            }

            OnServerClientConnected?.Invoke(client);
        }

        /// <summary>
        /// Sockets the server client disconnected handler.
        /// </summary>
        /// <param name="client">The client.</param>
        private void SocketServerClientDisconnectedHandler(UdpSender client)
        {
            Console.WriteLine($"Tcp:{client.Guid} client->server[{client.RemoteEndPoint}] disconnected");

            UdpConfig config = GetSocketConfig(client, UdpTypeFlags.Server);

            if (config != null) RemoveSender(config.Id);

            OnServerClientDisconnected?.Invoke(client);
        }

        /// <summary>
        /// Sockets the client connected handler.
        /// </summary>
        /// <param name="client">The client.</param>
        private void SocketClientConnectedHandler(UdpSender client)
        {
            Console.WriteLine($"Tcp:{client.Guid} server->client[{client.RemoteEndPoint}] connected");

            UdpConfig config = GetSocketConfig(client, UdpTypeFlags.Client);

            if (config != null)
            {
                RemoveSender(config.Id);

                senders.Add(config.Id, client);
            }

            OnClientConnected?.Invoke(client);
        }

        /// <summary>
        /// Sockets the client disconnected handler.
        /// </summary>
        /// <param name="client">The client.</param>
        private void SocketClientDisconnectedHandler(UdpSender client)
        {
            Console.WriteLine($"Tcp:{client.Guid} server->client[{client.RemoteEndPoint}] disconnected");

            UdpConfig config = GetSocketConfig(client, UdpTypeFlags.Client);

            if (config != null) RemoveSender(config.Id);

            OnClientDisconnected(client);
        }

        /// <summary>
        /// Sockets the exception handler.
        /// </summary>
        /// <param name="client">The client.</param>
        /// <param name="exception">The exception.</param>
        private void SocketExceptionHandler(UdpSender client, Exception exception)
        {
            Console.WriteLine($"Tcp:{client.Guid} Exception:{exception.Message}");

            OnException?.Invoke(client, exception);
        }

        /// <summary>
        /// Gets the socket configuration.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="flags">The flags.</param>
        /// <returns></returns>
        private UdpConfig GetSocketConfig(UdpSender sender,UdpTypeFlags flags)
        {
            UdpConfig config = null;
            IPEndPoint point = sender.RemoteEndPoint as IPEndPoint;

            switch (flags)
            {
                case UdpTypeFlags.Client:
                    config = cConfigs.FirstOrDefault(x => x.Host == point.Address.ToString() && x.Port == point.Port);
                    break;
                case UdpTypeFlags.Server:
                    config = scConfigs.FirstOrDefault(x => x.Host == point.Address.ToString() && (x.Port == 0 || x.Port == point.Port));
                    break;
                default:
                    break;
            }

            return config;
        }

        /// <summary>
        /// Removes the sender.
        /// </summary>
        /// <param name="id">The identifier.</param>
        private void RemoveSender(int id)
        {
            if (senders.ContainsKey(id)) senders.Remove(id);
        }

        /// <summary>
        /// Checkings the state.
        /// </summary>
        /// <param name="initialized">if set to <c>true</c> [initialized].</param>
        /// <param name="started">if set to <c>true</c> [started].</param>
        /// <param name="restarting">if set to <c>true</c> [restarting].</param>
        /// <exception cref="System.InvalidOperationException">
        /// The module is not initialized
        /// or
        /// The module is not started
        /// or
        /// The module is running
        /// </exception>
        private void CheckingState(bool initialized = false, bool started = false, bool restarting = false)
        {
            if (initialized && !this.initialized) throw new InvalidOperationException("The provider is not initialized");

            if (started && !this.started) throw new InvalidOperationException("The provider is not started");

            if (restarting && this.started) throw new InvalidOperationException("The provider is running");
        }

        /// <summary>
        /// Registers the modules.
        /// </summary>
        /// <param name="types">The types.</param>
        public void RegisterModules(Type[] types)
        {
            OnRegisterModules(types, (entry, handler) =>
            {
                if (!this.OnData.ContainsKey(entry)) this.OnData.Add(entry, null);
                this.OnData[entry] += handler.OnUdpDataHandler;
            });
        }

        /// <summary>
        /// Gets the TCP sender.
        /// </summary>
        /// <param name="id">The identifier.</param>
        /// <returns></returns>
        public UdpSender GetUdpSender(int id)
        {
            if (senders.ContainsKey(id)) return senders[id];

            return null;
        }
    }
}